/*
 *  This file is part of Bracket Properties
 *  Copyright 2011-2013 David R. Smith
 *  All Rights Reserved
 *
 */
package asia.redact.bracket.properties.serializer;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>
 * Use the java Reflection API to get values on a given token's target class. These values are then used
 * in serialization, generally into some type of formatted String.
 * </p>
 * <p>
 * There is one custom interface, Loadable. Loadable is very simple: it expects the toString() method
 * to output whatever is required to represent the data elements in that class. When Getter sees a Loadable,
 * it will call toString on the class and use that data with a key called fromString. Loadable has the method 
 * fromString(String rep). 
 * </p>
 * 
 * <p>General use:</p>
 * 
 * <pre>
 * 
 * 		Getter getter = new Getter(parentToken);
 * 		List<Method> gMethods = getter.listGetters();
 *		for(Method method: gMethods) {
 *			Token token = getter.invoke(method);
 *			if(token.getId()==0)continue;
 *			if(token.isRenderImmediate()){
 *				renderer.renderImmediate(parentToken, token);
 *			}else{
 *				renderer.renderIndirect(parentToken, token);
 *			    walk(token);
 *			}
 *		}
 * 
 * </pre>
 * 
 * 
 * @author Dave
 *
 *TODO make thread-safe
 *
 */
public class MethodGetter {

	final Token token;
	
	// lazy load and cache
	List<Method> methodList = null;
	
	private final Pattern getterPattern = Pattern.compile("(get|is)[A-Z][a-zA-Z0-9]+");
	
	public MethodGetter(Token token) {
		this.token = token;
	}
	
	public MethodGetter(Object obj) {
		token = TokenFactory.INSTANCE.createTmpToken(obj);
	}
	
	public static Method getEnumNameMethod() {
		Method m =null;
		try {
			m = Enum.class.getMethod("name", (Class<?>[])null);
		} catch (NoSuchMethodException | SecurityException e) {
			e.printStackTrace();
		}
		return m;
	}
	
	/**
	 * Return a list of all the "getter" accessors in token's class
	 * 
	 * Does not return "getClass"; checks for String and does not return getBytes or isEmpty
	 * 
	 * @return
	 */
	public List<Method> listGetters() {
		if(methodList == null) {
		methodList = new ArrayList<Method>();
		Class<?> clazz = token.getObject().getClass();
		Object instance = token.getObject();
		for(Method m : clazz.getMethods()) {
			
			if(deprecated(m)) continue; //skip all these
			if(m.getName().equals("getClass")) continue; 
			if(instance instanceof String && m.getName().equals("getBytes")) continue;
			if(instance instanceof String && m.getName().equals("isEmpty")) continue;
			
			Matcher matcher = getterPattern.matcher(m.getName());
			if(matcher.matches()){
				Class<?>[] params = m.getParameterTypes();
				if(params != null && params.length == 0){
					methodList.add(m);
				}
			}
		}
		return methodList;
		}else{
			return methodList;
		}
	}
	
	
	public Token invoke(Method m){
	    m.setAccessible(true);
		Object instance = token.getObject();
	    try {
			Object obj = m.invoke(instance, new Object[0]);
			if(obj == null) return TokenFactory.INSTANCE.nullToken();
			TokenType tt = testType(obj);
			String key = keyify(m.getName());
			Token token = TokenFactory.INSTANCE.createToken(tt, key, obj);
			
			if(tt == TokenType.A){
				long length = Array.getLength(obj);
				token.setArrayLength(length);
			}
			
			
			return token;
		} catch (Exception x) {
			System.err.println("instance failed:"+instance+"");
			throw new RuntimeException(x);
		}
	}
	
	public static TokenType testType(Object obj) {
		
		if(Loadable.class.isAssignableFrom(obj.getClass())) return TokenType.LOADABLE;
		
		if(obj.getClass().isPrimitive()) return TokenType.P;
		else if(obj.getClass().isArray()) return TokenType.A;
		else if(obj instanceof String) return TokenType.S;
		else if(obj instanceof Integer) return TokenType.I;
		else if(obj instanceof Long) return TokenType.LO;
		else if(obj instanceof Double) return TokenType.D;
		else if(obj instanceof Float) return TokenType.F;
		else if(obj instanceof Boolean) return TokenType.B;
		else if(obj instanceof List) return TokenType.LI;
		else if(obj instanceof Map) return TokenType.M;
		else if(obj instanceof Enum) return TokenType.E;
		else return TokenType.O;
	}
	
	private String keyify(String getter){
		StringBuffer buf = new StringBuffer();
		if(getter.startsWith("get")){
			String sub = getter.substring(3);
			buf.append(Character.toLowerCase(sub.charAt(0)));
			buf.append(sub.substring(1));
		}else if(getter.startsWith("is")) {
			String sub = getter.substring(2);
			buf.append(Character.toLowerCase(sub.charAt(0)));
			buf.append(sub.substring(1));
		}else{
			buf.append(getter);
		}
		return buf.toString();
	}
	
	private boolean deprecated(Method m){
		Annotation [] annotations = m.getAnnotations();
		for(Annotation a :annotations){
			if(a.toString().contains("Deprecated")) return true;
		}
		return false;
	}

}
